先看範例:
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; x: number }
  | { kind: "triangle"; x: number; y: number };
function area(s: Shape) {
  if (s.kind === "circle") {
    return Math.PI * s.radius * s.radius;
  } else if (s.kind === "square") {
    return s.x * s.x;
  } else {
    return (s.x * s.y) / 2;
  }
}
在這個範例中,Shape 是一個 Union Type。每個型別都有辨識屬性 kind,TypeScript 會根據這個屬性來區分型別。例如,當 kind 是 "circle" 時,TypeScript 可以推斷 s 一定有 radius 屬性,而不會有其他不相關的屬性。
透過 if 條件,TypeScript 可以縮小型別範圍,當判斷條件 s.kind === "circle" 為真時,TypeScript 會將 s 的型別縮小到 circle,這樣就可以安全地訪問 radius 屬性。
當然除了 if,switch 也有同樣的效果。
Type Guards : 是指在透過特定的邏輯判斷來縮小變數的型別範圍,有時也會搭配 typeof , instanceof , in 等關鍵字使用。
Narrowing : 是指將變數的型別從更廣泛的範圍縮小到具體型別的過程,常搭配 Type Guards 使用。
Discriminated Unions 適合在處理多種可能的狀態或多個不同類型的情境時使用。特別是當我們有多個型別之間有部份差異,但還是共享某些共同屬性時。常用於下列情境:
這邊我寫一個 UserCard 的 Component,總共分成三種情況:
type UserCardProps = {
  name: string;
  role: "user" | "admin" | "guest";
  permissions?: string[]; // 只有 admin 才有這個屬性,但這裡是可選的
  email?: string; // 只有 user 才有 email,但這裡也是可選的
};
export default function UserCard({ props }: { props: UserCardProps }) {
  const { name, role, permissions, email } = props;
  if (role === "admin") {
    return (
      <div>
        <h2>{name} (Admin)</h2>
        <p>Permissions: {permissions?.join(", ")}</p>
      </div>
    );
  }
  if (role === "user") {
    return (
      <div>
        <h2>{name} (User)</h2>
        <p>Email: {email}</p>
      </div>
    );
  }
  return (
    <div>
      <h2>{name} (Guest)</h2>
    </div>
  );
}
這個範例的問題:
role 是 'guest',有可能會意外傳入 permissions 或 email。permissions?.join(", ") 或 email 的時候,如果這些屬性沒有被傳入,結果將會是 undefined,這可能會導致顯示錯誤和其他問題。type AdminProps = {
  role: "admin";
  permissions: string[];
};
type UserProps = {
  role: "user";
  email: string;
};
type GuestProps = {
  role: "guest";
};
type UserCardProps = {
  name: string;
} & (AdminProps | UserProps | GuestProps);
export default function UserCard({ props }: { props: UserCardProps }) {
  const { name, role } = props; 
  // 如果在這邊直接取 permissions 或 email 會出現錯誤,因為 TypeScript 知道這些屬性不是通用的
  if (role === "admin") {
    const { permissions } = props; 
    // TypeScript 確保當 role 是 "admin" 時,permissions 一定存在
    return (
      <div>
        <h2>{name} (Admin)</h2>
        <p>Permissions: {permissions.join(", ")}</p>
      </div>
    );
  }
  if (role === "user") {
    const { email } = props; 
    // 這邊也是一樣,當 role 是 "user" 時,email 一定存在
    return (
      <div>
        <h2>{name} (User)</h2>
        <p>Email: {email}</p>
      </div>
    );
  }
  return (
    <div>
      <h2>{name} (Guest)</h2>
    </div>
  );
}
參考資料:
https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#discriminated-unions
https://www.typescriptlang.org/docs/handbook/2/narrowing.html
https://www.totaltypescript.com/discriminated-unions-are-a-devs-best-friend